// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:front_end/src/source/source_library_builder.dart';
import 'package:front_end/src/type_inference/type_constraint_gatherer.dart';
import 'package:front_end/src/type_inference/type_inference_engine.dart';
import 'package:front_end/src/type_inference/type_schema.dart';
import 'package:front_end/src/type_inference/type_schema_environment.dart';
import 'package:kernel/ast.dart';
import 'package:kernel/class_hierarchy.dart';
import 'package:kernel/core_types.dart';
import 'package:kernel/testing/type_parser_environment.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

void main() {
  defineReflectiveSuite(() {
    defineReflectiveTests(TypeConstraintGathererTest);
  });
}

@reflectiveTest
class TypeConstraintGathererTest {
  late Env env;

  final Map<String, DartType Function()> additionalTypes = {
    'UNKNOWN': () => UnknownType()
  };

  late Library _coreLibrary;

  late Library _testLibrary;

  TypeConstraintGathererTest();

  Component get component => env.component;

  CoreTypes get coreTypes => env.coreTypes;

  Library get coreLibrary => _coreLibrary;

  Library get testLibrary => _testLibrary;

  void parseTestLibrary(String testLibraryText) {
    env = new Env(testLibraryText);
    assert(
        env.component.libraries.length == 2,
        "The tests are supposed to have exactly two libraries: "
        "the core library and the test library.");
    Library firstLibrary = env.component.libraries.first;
    Library secondLibrary = env.component.libraries.last;
    if (firstLibrary.importUri.isScheme("dart") &&
        firstLibrary.importUri.path == "core") {
      _coreLibrary = firstLibrary;
      _testLibrary = secondLibrary;
    } else {
      assert(
          secondLibrary.importUri.isScheme("dart") &&
              secondLibrary.importUri.path == "core",
          "One of the libraries is expected to be 'dart:core'.");
      _coreLibrary == secondLibrary;
      _testLibrary = firstLibrary;
    }
  }

  void test_any_subtype_parameter() {
    parseTestLibrary('class P; class Q;');

    checkConstraintsLower('T1', 'Q', ['lib::Q <: T1'],
        typeParameters: 'T1 extends Object?');
  }

  void test_any_subtype_top() {
    parseTestLibrary('class P; class Q;');

    checkConstraintsUpper('P', 'dynamic', []);
    checkConstraintsUpper('P', 'Object', []);
    checkConstraintsUpper('P', 'Object?', []);
    checkConstraintsUpper('P', 'void', []);
  }

  void test_any_subtype_unknown() {
    parseTestLibrary('class P; class Q;');

    checkConstraintsUpper('P', 'UNKNOWN', []);
    checkConstraintsUpper('P?', 'UNKNOWN', []);
    checkConstraintsUpper('T1', 'UNKNOWN', [],
        typeParameters: 'T1 extends Object?');
    checkConstraintsUpper('T1?', 'UNKNOWN', [],
        typeParameters: 'T1 extends Object?');
  }

  void test_different_classes() {
    parseTestLibrary('class P; class Q;');

    checkConstraintsUpper('List<T1>', 'Iterable<Q>', ['T1 <: lib::Q'],
        typeParameters: 'T1 extends Object?');
    checkConstraintsUpper('Iterable<T1>', 'List<Q>', null,
        typeParameters: 'T1 extends Object?');
  }

  void test_equal_types() {
    parseTestLibrary('class P; class Q;');

    checkConstraintsUpper('P', 'P', []);
  }

  void test_function_generic() {
    parseTestLibrary('');

    // <T>() -> dynamic <: () -> dynamic, never
    checkConstraintsUpper(
        '<T extends Object?>() -> dynamic', '() -> dynamic', null);
    // () -> dynamic <: <T>() -> dynamic, never
    checkConstraintsUpper(
        '() -> dynamic', '<T extends Object?>() -> dynamic', null);
    // <T>(T) -> T <: <U>(U) -> U, always
    checkConstraintsUpper(
        '<T extends Object?>(T) -> T', '<U extends Object?>(U) -> U', []);
  }

  void test_function_parameter_mismatch() {
    parseTestLibrary('class P; class Q;');

    // (P) -> dynamic <: () -> dynamic, never
    checkConstraintsUpper('(P) -> dynamic', '() -> dynamic', null);
    // () -> dynamic <: (P) -> dynamic, never
    checkConstraintsUpper('() -> dynamic', '(P) -> dynamic', null);
    // ([P]) -> dynamic <: () -> dynamic, always
    checkConstraintsUpper('([P]) -> dynamic', '() -> dynamic', []);
    // () -> dynamic <: ([P]) -> dynamic, never
    checkConstraintsUpper('() -> dynamic', '([P]) -> dynamic', null);
    // ({x: P}) -> dynamic <: () -> dynamic, always
    checkConstraintsUpper('({P x}) -> dynamic', '() -> dynamic', []);
    // () -> dynamic !<: ({x: P}) -> dynamic, never
    checkConstraintsUpper('() -> dynamic', '({P x}) -> dynamic', null);
  }

  void test_function_parameter_types() {
    parseTestLibrary('class P; class Q;');

    // (T1) -> dynamic <: (Q) -> dynamic, under constraint Q <: T1
    checkConstraintsUpper('(T1) -> dynamic', '(Q) -> dynamic', ['lib::Q <: T1'],
        typeParameters: 'T1 extends Object?');
    // (T1?) -> dynamic <: (Q?) -> dynamic, under constraint Q <: T1
    checkConstraintsUpper(
        '(T1?) -> dynamic', '(Q?) -> dynamic', ['lib::Q <: T1'],
        typeParameters: 'T1 extends Object?');
    // ({x: T1}) -> dynamic <: ({x: Q}) -> dynamic, under constraint Q <: T1
    checkConstraintsUpper(
        '({T1 x}) -> dynamic', '({Q x}) -> dynamic', ['lib::Q <: T1'],
        typeParameters: 'T1 extends Object?');
    // ({x: T1?}) -> dynamic <: ({x: Q?}) -> dynamic, under constraint Q <: T1
    checkConstraintsUpper(
        '({T1? x}) -> dynamic', '({Q? x}) -> dynamic', ['lib::Q <: T1'],
        typeParameters: 'T1 extends Object?');
  }

  void test_function_return_type() {
    parseTestLibrary('class P; class Q;');

    // () -> T1 <: () -> Q, under constraint T1 <: Q
    checkConstraintsUpper('() -> T1', '() -> Q', ['T1 <: lib::Q'],
        typeParameters: 'T1 extends Object?');
    // () -> T1? <: () -> Q?, under constraint T1 <: Q
    checkConstraintsUpper('() -> T1?', '() -> Q?', ['T1 <: lib::Q'],
        typeParameters: 'T1 extends Object?');
    // () -> P <: () -> void, always
    checkConstraintsUpper('() -> P', '() -> void', []);
    // () -> P? <: () -> void, always
    checkConstraintsUpper('() -> P?', '() -> void', []);
    // () -> void <: () -> P, never
    checkConstraintsUpper('() -> void', '() -> P', null);
    // () -> void <: () -> P?, never
    checkConstraintsUpper('() -> void', '() -> P?', null);
  }

  void test_function_trivial_cases() {
    parseTestLibrary('');

    // () -> dynamic <: dynamic, always
    checkConstraintsUpper('() -> dynamic', 'dynamic', []);
    // () -> dynamic <: Function, always
    checkConstraintsUpper('() -> dynamic', "Function", []);
    // () -> dynamic <: Object, always
    checkConstraintsUpper('() -> dynamic', 'Object', []);
  }

  void test_nonInferredParameter_subtype_any() {
    parseTestLibrary('class P; class Q;');

    checkConstraintsLower('List<T1>', 'U', ['lib::P <: T1'],
        typeParameters: 'T1 extends Object, U extends List<P>',
        typeParametersToConstrain: 'T1');
    checkConstraintsLower('List<T1?>', 'U', ['lib::P <: T1'],
        typeParameters: 'T1 extends Object, U extends List<P?>',
        typeParametersToConstrain: 'T1');
  }

  void test_null_subtype_any() {
    parseTestLibrary('class P; class Q;');

    checkConstraintsLower('T1', 'Null', ['Null <: T1'],
        typeParameters: 'T1 extends Object?');
    checkConstraintsLower('T1?', 'Null', ['Null <: T1'],
        typeParameters: 'T1 extends Object?');
    checkConstraintsUpper('Null', 'Q?', []);
  }

  void test_parameter_subtype_any() {
    parseTestLibrary('class P; class Q;');

    checkConstraintsUpper('T1', 'Q', ['T1 <: lib::Q'],
        typeParameters: 'T1 extends Object?');
  }

  void test_same_classes() {
    parseTestLibrary('class P; class Q;');

    checkConstraintsUpper('List<T1>', 'List<Q>', ['T1 <: lib::Q'],
        typeParameters: 'T1 extends Object?');
    checkConstraintsUpper('List<T1?>', 'List<Q?>', ['T1 <: lib::Q'],
        typeParameters: 'T1 extends Object?');
    checkConstraintsUpper('List<T1>?', 'List<Q>?', ['T1 <: lib::Q'],
        typeParameters: 'T1 extends Object?');
    checkConstraintsUpper('List<T1?>?', 'List<Q?>?', ['T1 <: lib::Q'],
        typeParameters: 'T1 extends Object?');
  }

  void test_typeParameters() {
    parseTestLibrary('class P; class Q; class Map<X, Y>;');

    checkConstraintsUpper(
        'Map<T1, T2>', 'Map<P, Q>', ['T1 <: lib::P', 'T2 <: lib::Q'],
        typeParameters: 'T1 extends Object?, T2 extends Object?');
    checkConstraintsUpper(
        'Map<T1, T2>', 'Map<P?, Q?>', ['T1 <: lib::P?', 'T2 <: lib::Q?'],
        typeParameters: 'T1 extends Object?, T2 extends Object?');
    checkConstraintsUpper('Map<T1?, T2?>', 'Map<P, Q>', null,
        typeParameters: 'T1 extends Object?, T2 extends Object?');
    checkConstraintsUpper(
        'Map<T1?, T2?>', 'Map<P?, Q?>', ['T1 <: lib::P', 'T2 <: lib::Q'],
        typeParameters: 'T1 extends Object?, T2 extends Object?');
    checkConstraintsUpper(
        'Map<T1, T2>?', 'Map<P, Q>?', ['T1 <: lib::P', 'T2 <: lib::Q'],
        typeParameters: 'T1 extends Object?, T2 extends Object?');
    checkConstraintsUpper(
        'Map<T1, T2>?', 'Map<P?, Q?>?', ['T1 <: lib::P?', 'T2 <: lib::Q?'],
        typeParameters: 'T1 extends Object?, T2 extends Object?');
    checkConstraintsUpper('Map<T1?, T2?>?', 'Map<P, Q>?', null,
        typeParameters: 'T1 extends Object?, T2 extends Object?');
    checkConstraintsUpper(
        'Map<T1?, T2?>?', 'Map<P?, Q?>?', ['T1 <: lib::P', 'T2 <: lib::Q'],
        typeParameters: 'T1 extends Object?, T2 extends Object?');
  }

  void test_unknown_subtype_any() {
    parseTestLibrary('class P; class Q;');

    checkConstraintsUpper('Q', 'UNKNOWN', []);
    checkConstraintsUpper('Q?', 'UNKNOWN', []);
    checkConstraintsUpper('T1', 'UNKNOWN', [],
        typeParameters: 'T1 extends Object?');
    checkConstraintsUpper('T1?', 'UNKNOWN', [],
        typeParameters: 'T1 extends Object?');
  }

  void checkConstraintsLower(String type, String bound, List<String>? expected,
      {String? typeParameters, String? typeParametersToConstrain}) {
    env.withStructuralParameters(typeParameters ?? '',
        (List<StructuralParameter> typeParameterNodes) {
      List<StructuralParameter> typeParameterNodesToConstrain;
      if (typeParametersToConstrain != null) {
        Set<String> namesToConstrain =
            typeParametersToConstrain.split(",").map((s) => s.trim()).toSet();
        typeParameterNodesToConstrain = typeParameterNodes
            .where((p) => namesToConstrain.contains(p.name))
            .toList();
      } else {
        typeParameterNodesToConstrain = typeParameterNodes;
      }
      _checkConstraintsLowerTypes(
          env.parseType(type, additionalTypes: additionalTypes),
          env.parseType(bound, additionalTypes: additionalTypes),
          testLibrary,
          expected,
          typeParameterNodesToConstrain);
    });
  }

  void _checkConstraintsLowerTypes(
      DartType type,
      DartType bound,
      Library clientLibrary,
      List<String>? expectedConstraints,
      List<StructuralParameter> typeParameterNodesToConstrain) {
    _checkConstraintsHelper(
        type,
        bound,
        clientLibrary,
        expectedConstraints,
        (gatherer, type, bound) =>
            gatherer.tryConstrainLower(type, bound, treeNodeForTesting: null),
        typeParameterNodesToConstrain);
  }

  void checkConstraintsUpper(String type, String bound, List<String>? expected,
      {String? typeParameters, String? typeParametersToConstrain}) {
    env.withStructuralParameters(typeParameters ?? '',
        (List<StructuralParameter> typeParameterNodes) {
      List<StructuralParameter> typeParameterNodesToConstrain;
      if (typeParametersToConstrain != null) {
        Set<String> namesToConstrain =
            typeParametersToConstrain.split(",").map((s) => s.trim()).toSet();
        typeParameterNodesToConstrain = typeParameterNodes
            .where((p) => namesToConstrain.contains(p.name))
            .toList();
      } else {
        typeParameterNodesToConstrain = typeParameterNodes;
      }
      _checkConstraintsUpperTypes(
          env.parseType(type, additionalTypes: additionalTypes),
          env.parseType(bound, additionalTypes: additionalTypes),
          testLibrary,
          expected,
          typeParameterNodesToConstrain);
    });
  }

  void _checkConstraintsUpperTypes(
      DartType type,
      DartType bound,
      Library clientLibrary,
      List<String>? expectedConstraints,
      List<StructuralParameter> typeParameterNodesToConstrain) {
    _checkConstraintsHelper(
        type,
        bound,
        clientLibrary,
        expectedConstraints,
        (gatherer, type, bound) =>
            gatherer.tryConstrainUpper(type, bound, treeNodeForTesting: null),
        typeParameterNodesToConstrain);
  }

  void _checkConstraintsHelper(
      DartType a,
      DartType b,
      Library clientLibrary,
      List<String>? expectedConstraints,
      bool Function(TypeConstraintGatherer, DartType, DartType) tryConstrain,
      List<StructuralParameter> typeParameterNodesToConstrain) {
    var typeSchemaEnvironment = new TypeSchemaEnvironment(
        coreTypes, new ClassHierarchy(component, coreTypes));
    var typeConstraintGatherer = new TypeConstraintGatherer(
        typeSchemaEnvironment, typeParameterNodesToConstrain,
        typeOperations: new OperationsCfe(typeSchemaEnvironment,
            fieldNonPromotabilityInfo: new FieldNonPromotabilityInfo(
                fieldNameInfo: {}, individualPropertyReasons: {}),
            typeCacheNonNullable: {},
            typeCacheNullable: {},
            typeCacheLegacy: {}),
        inferenceResultForTesting: null,
        inferenceUsingBoundsIsEnabled: false);
    var constraints = tryConstrain(typeConstraintGatherer, a, b)
        ? typeConstraintGatherer.computeConstraints()
        : null;
    if (expectedConstraints == null) {
      expect(constraints, isNull);
      return;
    }
    expect(constraints, isNotNull);
    var constraintStrings = <String>[];
    constraints!.forEach((t, constraint) {
      if (constraint.lower is! UnknownType ||
          constraint.upper is! UnknownType) {
        var s = t.name;
        if (constraint.lower is! UnknownType) {
          s = '${typeSchemaToString(
            constraint.lower.unwrapTypeSchemaView(),
          )}'
              ' <: $s';
        }
        if (constraint.upper is! UnknownType) {
          s = '$s <: '
              '${typeSchemaToString(
            constraint.upper.unwrapTypeSchemaView(),
          )}';
        }
        constraintStrings.add(s as String);
      }
    });
    expect(constraintStrings, unorderedEquals(expectedConstraints));
  }
}
